<?php
// @formatter:off
/**
 * @file WCParser.php
 *
 * @author Alejandro Dario Simi
 * @date $Date: 2013-06-04 03:02:53 +0000 (Tue, 04 Jun 2013) $
 *
 * $Id: WCParser.php 68 2013-06-04 03:02:53Z daemonraco@gmail.com $
 * $URL: http://wcomix.googlecode.com/svn/tags/wcomix-1.0-BETA2/includes/parser/WCParser.php $
 */
// @formatter:on

require_once dirname(__FILE__)."/WCParserNode.php";
require_once dirname(__FILE__)."/WCParserBehaviorIGNORE.php";
require_once dirname(__FILE__)."/WCParserBehaviorVAR.php";
require_once dirname(__FILE__)."/WCParserBehaviorIF.php";
require_once dirname(__FILE__)."/WCParserBehaviorISSET.php";
require_once dirname(__FILE__)."/WCParserBehaviorINCLUDE.php";
require_once dirname(__FILE__)."/WCParserBehaviorOVER.php";

/**
 * @class WCParser
 * This is a main class that provides a text parsing system based on tags.
 * This class and those that compose it work on the idea of the next pseudo-code:
 * @code
 * 	Some text <:FUNC:parametres:/>contents<::/> and some more text.
 * @endcode
 */
class WCParser {
	/** @var string This is the indicator for any tag that is been open. */
	public static $TagOpen = "<:";
	/** @var string This is the indicator for any tag that is been closed. */
	public static $TagClose = ":/>";
	/**
	 * @var string This is the separator of parameter inside a tag
	 * specification.
	 */
	public static $ParamSeparator = ":";

	public static $WarningUNSET_VARIABLE = "UNSET_VARIABLE";

	/**
	 * @var string
	 * This is a sort of cache of the last code generated by the method parse.
	 */
	protected $_code = false;
	protected $_maxDepth = 10;
	/**
	 * @var WCParserNode[]
	 * This is the list of every node held by this class. This is a array
	 * representation of nodes.
	 */
	protected $_nodes = null;
	protected $_ok = false;
	protected $_path = false;
	/**
	 * @var WCParserNode
	 * This is the root for all node held by this class. This is a tree
	 * representation of nodes.
	 */
	protected $_root = null;
	protected $_showSummary = true;
	protected $_showWarnings = true;
	protected $_templatesRoot = "";
	/**
	 * @var string[]
	 * List of variables and their values to be used when parsing text.
	 */
	protected $_variables = array();
	protected $_warnings = array();

	//
	// Constructors.
	public function __construct() {
		$this->setWarning(WCParser::$WarningUNSET_VARIABLE, false, false, "Requested variables that have not been set");
	}
	//
	// Public Special Methods.
	/**
	 * This method allows to get the value of certain variable previously set.
	 *
	 * @var string $varname Name of a set variable to get.
	 * @return mixed Returns it's value unless it's not set, in that case, it
	 * return null, adds the error to the warnings list and, if it's enabled,
	 * shows triggers a warning message.
	 */
	public function __get($varname) {
		if($this->has($varname)) {
			return $this->_variables[$varname];
		} else {
			$this->setWarning(WCParser::$WarningUNSET_VARIABLE, "Variable '{$varname}' is not set", false, false, __CLASS__);
			if($this->_showWarnings) {
				trigger_error(__CLASS__." has no variable '{$varname}' set", E_USER_WARNING);
			}
			return null;
		}
	}
	/**
	 * Sets a variable to use when parsing text.
	 *
	 * @param string $varname Name for the variable to set.
	 * @param string $value Value to associate with the variable.
	 */
	public function __set($varname, $value) {
		$this->_variables[$varname] = $value;
	}
	//
	// Public Methods.
	/**
	 * This is a more polite way to set a variable, but in the end, it's
	 * similar to __set().
	 *
	 * @param string $varname Name for the variable to set.
	 * @param string $value Value to associate with the variable.
	 */
	public function assign($varname, &$value) {
		$this->_variables[$varname] = $value;
	}
	/**
	 * Checks if a variable has been set.
	 *
	 * @param string $varname Name of the variable to be checked.
	 * @return boolean Returns true if the variable has been set.
	 */
	public function has($varname) {
		return isset($this->_variables[$varname]);
	}
	/**
	 * This method allows to load the parser with the text to be parsed. Such
	 * file is also called template.
	 *
	 * @param string $file Template path relative to $this->_templatesRoot. It
	 * always must start with '/' (slash).
	 * @param string $full If this parameter is true, $this->_templatesRoot is
	 * ignored and $file is considered as full path.
	 * @return boolean Returns true when it loaded without problems.
	 */
	public function load($file, $full = false) {
		$this->_path = ($full ? "" : $this->_templatesRoot).$file;
		//
		// If it's not possible to read a template file, WCParser triggers
		// an abort signal.
		if(!is_readable($this->_path)) {
			trigger_error("Unable to read file '{$this->_path}'", E_USER_ERROR);
		}

		//
		// Cleaning previous parsing data.
		// @{
		if($this->_root != null) {
			$this->_root = null;
		}
		$this->_nodes = array();
		$this->_code = false;
		// @}
		//
		// Loading file contents.
		$content = file_get_contents($this->_path);

		$openLen = strlen(WCParser::$TagOpen);
		$closeLen = strlen(WCParser::$TagClose);

		$id = 0;
		$len = strlen($content);

		//
		// Creating root node.
		$this->_root = new WCParserNode($id++, $this);
		$currId = $id++;
		//
		// This stack tracks parent nodes when nesting.
		$parents = array();
		//
		// Analyzes all the contents until there's no more.
		while($content) {
			//
			// If the current node is not yet set in the nodes' list
			// it is created and added.
			if(!isset($this->_nodes[$currId])) {
				$this->_nodes[$currId] = new WCParserNode($currId, $this);
				$this->_root->addChild($this->_nodes[$currId]);
			}
			//
			// Searching for a tag opening mark.
			$openPos = strpos($content, WCParser::$TagOpen);

			if($openPos > 0) {
				//
				// Lasting text before the tag is added to the
				// current node.
				$aux = substr($content, 0, $openPos);
				$this->_nodes[$currId]->addChild($aux);
				//
				// Current contents are reduced.
				$content = substr($content, $openPos);
			} elseif($openPos === false) {
				//
				// If the search reaches the end of all contents,
				// it adds the rest of the text to the current
				// node.
				$this->_nodes[$currId]->addChild($content);
				$content = "";
			} else {
				// If the current content starts with an opening
				// tag mark, it analyzes it
				//
				// Getting the tag closing mark.
				$closePos = strpos($content, WCParser::$TagClose);
				if($closePos === false) {
					//
					// If the tag closing mark isn't found
					// could cause errors, therefore the
					// process aborts.
					trigger_error("Tag closing mark '{WCParser::$TagClose}' hasn't been found in '{$content}'", E_USER_ERROR);
				}
				// Getting the command to be performed.
				$command = substr($content, $openLen, $closePos - $closeLen + 1);
				// Current content is reduced.
				$content = substr($content, $closePos + $closeLen);
				//
				// if $command is not empty, it means it's not a
				// closing tag.
				if($command) {
					//
					// Expanding the command.
					$command = explode(WCParser::$ParamSeparator, $command);
					// Creating a node to manage this
					// subsection.
					$auxNode = new WCParserNode($id, $this);
					// Setting the current node as it's parent.
					$auxNode->parentNode($this->_nodes[$currId]);
					// The first element of the expanded
					// command is the function name.
					$auxNode->func(array_shift($command));
					// The rest is the paramentres to be used
					// by the function.
					$auxNode->params($command);

					// Adding the current node id to the
					// parents stack.
					array_push($parents, $currId);
					// From now on, the current id is the new
					// one.
					$currId = $id;

					// Adding the new node to the list.
					$this->_nodes[$id++] = $auxNode;
				} else {
					// This is a closing tag.

					$auxId = $currId;
					// Jumping back the current id to the
					// parent based on the stack.
					$currId = array_pop($parents);
					//
					// Until now, the last created node is an
					// orphan one and since this is a closing
					// for it's specification, it is added to
					// the last parent.
					if(isset($this->_nodes[$currId]) && isset($this->_nodes[$auxId])) {
						$this->_nodes[$currId]->addChild($this->_nodes[$auxId]);
					} else {
						// If any of he nodes is broken
						// means something went wrong and
						// the process must abort.
						if(isset($this->_nodes[$currId])) {
							trigger_error("current node '{$currId}' is not set", E_USER_ERROR);
						} elseif(isset($this->_nodes[$auxId])) {
							trigger_error("Node '{$auxId}' is not set", E_USER_ERROR);
						}
					}
				}
			}
		}

		//
		// Checking if every node is OK.
		$this->_ok = true;
		foreach($this->_nodes as $id => $node) {
			if(!($this->_ok = $node->isOk())) {
				break;
			}
		}

		return $this->_ok;
	}
	/**
	 * Instead of assigning values for each variable, this method allows to
	 * set many variables in one call.
	 *
	 * @param array $values Associative array containing each variable to be
	 * assigned, where keys are names and values are exactly that.
	 * @param boolean $replace When this variable is true, the current list of
	 * variables is replaces by $values.
	 */
	public function massiveAssign($values = array(), $replace = false) {
		if($replace) {
			$this->_variables = array();
		}
		$this->_variables = array_merge($this->_variables, $values);
	}
	/**
	 * Returns the value of how deep can be a nested tag.
	 *
	 * @return int Nesting limit.
	 */
	public function maxDepth() {
		return $this->_maxDepth;
	}
	/**
	 * Sets the object to avoid prompting a summary at the end when a template
	 * is parsed.
	 *
	 * @param boolean $value When true disables the summary.
	 */
	public function noSummary($value = true) {
		$this->_showSummary = !$value;
	}
	/**
	 * Sets the object to avoid prompting warnings.
	 *
	 * @param boolean $value When true disables warnings.
	 */
	public function noWarnings($value = true) {
		$this->_showWarnings = !$value;
	}
	/**
	 * Current status of the object.
	 *
	 * @return boolean Returns true when the object had no problems until the
	 * moment, or, if there were any and now are corrected.
	 */
	public function ok() {
		return $this->_ok;
	}
	/**
	 * This method executes all nodes and obtain from them a clear text.
	 *
	 * @return string Returns the resulting text.
	 */
	public function parse() {
		if($this->_code === false) {
			if($this->ok() && $this->_root != null && $this->_root->isOk()) {
				//
				// Executing root node.
				$this->_code = $this->_root->parse();
			} elseif(!$this->ok()) {
				trigger_error("The parser is not right", E_USER_ERROR);
			} elseif($this->_root == null) {
				trigger_error("Root node is not set", E_USER_ERROR);
			} else {
				trigger_error("Root node is not right", E_USER_ERROR);
			}
			//
			// Adding a summary of the parsing process. Only if it's
			// not disabled.
			if($this->_showSummary) {
				$this->_code .= $this->summary();
			}
		}

		return $this->_code;
	}
	/**
	 * Force the object to re-parse.
	 *
	 * @return string Returns the resulting text.
	 */
	public function reparse() {
		$this->_code = false;
		return $this->parse();
	}
	/**
	 * Cleans variables and cached texts.
	 */
	public function reset() {
		$this->_code = false;
		$this->_variables = array();
	}
	/**
	 * Sets the value of how deep can be a nested tag.
	 *
	 * @return int $depth Nesting limit to be set.
	 * @return int Nesting limit.
	 */
	public function setMaxDepth($depth) {
		$this->_maxDepth = $depth;
		return $this->maxDepth();
	}
	public function setWarning($type, $value = false, $code = false, $description = false, $reporter = false) {
		if(!isset($this->_warnings[$type])) {
			$this->_warnings[$type] = array(
				"type" => $type,
				"list" => array(),
				"description" => false
			);
		}
		if($description !== false) {
			$this->_warnings[$type]["description"] = $description;
		}
		if($value !== false) {
			$this->_warnings[$type]["list"][] = ($code !== false ? "[{$code}] " : "").$value.($reporter !== false ? " [reported by {$reporter}] " : "");
		}
	}
	/**
	 * @return boolean Returns true when the summary is disables.
	 */
	public function showSummary() {
		return $this->_showSummary;
	}
	/**
	 * @return boolean Returns true when warnings is disables.
	 */
	public function showWarnings() {
		return $this->_showWarnings;
	}
	/**
	 * This methods allows to obtaing the current templates root path set in
	 * the object.
	 *
	 * @param string $value When it is not false, it sets the root path with
	 * it. It must be an absolute path and a existing directory.
	 * @return string Returns the current root path.
	 */
	public function templatesRoot($value = false) {
		if($value !== false) {
			$value = WCSanitizer::SystemPath($value);

			if(is_dir($value) and WCSanitizer::IsAbsolutePath($value)) {
				$this->_templatesRoot = $value;
			} elseif(!is_dir($value)) {
				trigger_error("'{$value}' is not a directory", E_USER_ERROR);
			} else {
				trigger_error("'{$value}' is not an absolute path", E_USER_ERROR);
			}
		}
		return $this->_templatesRoot;
	}
	/**
	 * Return the list of variables currently set in the object.
	 *
	 * @return string[] List of variables.
	 */
	public function variables() {
		return $this->_variables;
	}
	//
	// Protected Methods.
	/**
	 * Generates a summary with all warnings and errors found on the parsing
	 * process.
	 *
	 * @return string Returns a string containing the summary in HTML format.
	 */
	protected function summary() {
		$aux = "\n<div class=\"WCParserSummary\">\n";

		$aux .= "\t<h4\">Summary for '{$this->_path}':</h4>\n";

		if(count($this->_warnings)) {
			foreach($this->_warnings as $warning) {
				if(count($warning["list"])) {
					$aux .= "\t<p><label".($warning["description"] ? " title=\"{$warning["description"]}\"" : "").">'{$warning["type"]}' warnings:</label><ul>\n";
					foreach($warning["list"] as $item) {
						$aux .= "\t\t<li>{$item}</li>\n";
					}
					$aux .= "\t</ul></p>\n";
				}
			}
		}
		$aux .= "</div>\n";

		return $aux;
	}
}
?>